/**
 * Copyleft c==8 2024 acetone - The Samty project
 * Copyright (c) 2019-2022 polistern
 * Copyright (c) 2017 The I2P Project
 * Copyright (c) 2013-2015 The Anoncoin Core developers
 * Copyright (c) 2012-2013 giv
 *
 * Distributed under the MIT software license, see the accompanying
 * file LICENSE or http://www.opensource.org/licenses/mit-license.php.
 */

#pragma once

#include "compat.h"
#include "identity.h"

#include <cstdint>
#include <iostream>
#include <list>
#include <memory>
#include <string>
#include <utility>
#include <vector>

namespace Samty
{

constexpr uint32_t SAM_BUFSIZE           = 64*1024;
constexpr char SAM_DEFAULT_DESTINATION[] = "TRANSIENT";

constexpr char SAM_DEFAULT_SIGNATURE_TYPE[] = "7"; // EdDSA_SHA512_Ed25519
constexpr char SAM_BLINDED_SIGNATURE_TYPE[] = "11"; // RedDSA_SHA512_Ed25519

typedef u_int SOCKET;

struct Configuration
{
    std::string nickname = "Samty";
    std::string SAMHost = "127.0.0.1";
    uint16_t SAMPort = 7656;
    uint8_t inboundLength = 3;      // Possible correct values: 0-8
    uint8_t inboundQuantity = 3;    // Possible correct values: 1-16
    uint8_t inboundVariance = 0;    // Possible correct values: 0-3
    uint8_t outboundLength = 3;     // Possible correct values: 0-8
    uint8_t outboundQuantity = 3;   // Possible correct values: 1-16
    uint8_t outboundVariance = 0;   // Possible correct values: 0-3
    bool encryptedLeaseSet = false; // https://geti2p.net/spec/encryptedleaseset
    bool publishLeaseSet = true;    // Cannot initiate a connection from the outside if false

    std::string destination = SAM_DEFAULT_DESTINATION; // Using known keys will allow the static address to be used
};

class Message
{
public:
    enum eStatus
    {
        OK,
        EMPTY_ANSWER,
        CLOSED_SOCKET,
        CANNOT_PARSE_ERROR,
        /** The destination is already in use
       *
       * ->  SESSION CREATE ...
       * <-  SESSION STATUS RESULT=DUPLICATED_DEST
       */
        DUPLICATED_DEST,
        /**
       * The nickname is already associated with a session
       *
       * ->  SESSION CREATE ...
       * <-  SESSION STATUS RESULT=DUPLICATED_ID
       */
        DUPLICATED_ID,
        /**
       * A generic I2P error (e.g. I2CP disconnection, etc.)
       *
       * ->  HELLO VERSION ...
       * <-  HELLO REPLY RESULT=I2P_ERROR MESSAGE={$message}
       *
       * ->  SESSION CREATE ...
       * <-  SESSION STATUS RESULT=I2P_ERROR MESSAGE={$message}
       *
       * ->  STREAM CONNECT ...
       * <-  STREAM STATUS RESULT=I2P_ERROR MESSAGE={$message}
       *
       * ->  STREAM ACCEPT ...
       * <-  STREAM STATUS RESULT=I2P_ERROR MESSAGE={$message}
       *
       * ->  STREAM FORWARD ...
       * <-  STREAM STATUS RESULT=I2P_ERROR MESSAGE={$message}
       *
       * ->  NAMING LOOKUP ...
       * <-  NAMING REPLY RESULT=INVALID_KEY NAME={$name} MESSAGE={$message}
       */
        I2P_ERROR,
        /**
       * Stream session ID doesn't exist
       *
       * ->  STREAM CONNECT ...
       * <-  STREAM STATUS RESULT=INVALID_ID MESSAGE={$message}
       *
       * ->  STREAM ACCEPT ...
       * <-  STREAM STATUS RESULT=INVALID_ID MESSAGE={$message}
       *
       * ->  STREAM FORWARD ...
       * <-  STREAM STATUS RESULT=INVALID_ID MESSAGE={$message}
       */
        INVALID_ID,
        /**
       * The destination is not a valid private destination key
       *
       * ->  SESSION CREATE ...
       * <-  SESSION STATUS RESULT=INVALID_KEY MESSAGE={$message}
       *
       * ->  STREAM CONNECT ...
       * <-  STREAM STATUS RESULT=INVALID_KEY MESSAGE={$message}
       *
       * ->  NAMING LOOKUP ...
       * <-  NAMING REPLY RESULT=INVALID_KEY NAME={$name} MESSAGE={$message}
       */
        INVALID_KEY,
        /**
       * The peer exists, but cannot be reached
       *
       * ->  STREAM CONNECT ...
       * <-  STREAM STATUS RESULT=CANT_REACH_PEER MESSAGE={$message}
       */
        CANT_REACH_PEER,
        /**
       * Timeout while waiting for an event (e.g. peer answer)
       *
       * ->  STREAM CONNECT ...
       * <-  STREAM STATUS RESULT=TIMEOUT MESSAGE={$message}
       */
        TIMEOUT,
        /**
       * The SAM bridge cannot find a suitable version
       *
       * ->  HELLO VERSION ...
       * <-  HELLO REPLY RESULT=NOVERSION MESSAGE={$message}
       */
        NOVERSION,
        /**
       * The naming system can't resolve the given name
       *
       * ->  NAMING LOOKUP ...
       * <-  NAMING REPLY RESULT=INVALID_KEY NAME={$name} MESSAGE={$message}
       */
        KEY_NOT_FOUND,
        /**
       * The peer cannot be found on the network
       *
       * ??
       */
        PEER_NOT_FOUND,
        /**
       * ??
       *
       * ->  STREAM ACCEPT
       * <-  STREAM STATUS RESULT=ALREADY_ACCEPTING
       */
        ALREADY_ACCEPTING,
        /**
       * ??
       */
        FAILED,
        /**
       * ??
       */
        CLOSED
    };

    template<class T>
    struct Answer
    {
        const Message::eStatus status;
        T value;

        Answer(Message::eStatus status, const T &value)
            : status(status), value(value) {}
        explicit Answer(Message::eStatus status) : status(status), value() {}
    };

    static std::string hello(const std::string &minVer,
                             const std::string &maxVer);
    // Stream session
    static std::string
    sessionCreate(const std::string &sessionID,
                  const std::string &nickname,
                  const std::string &destination = SAM_DEFAULT_DESTINATION,
                  const std::string &options = "",
                  const std::string &signatureType = SAM_DEFAULT_SIGNATURE_TYPE);
    static std::string streamAccept(const std::string &sessionID,
                                    bool silent = false);
    static std::string streamConnect(const std::string &sessionID,
                                     const std::string &destination,
                                     bool silent = false);
    static std::string streamForward(const std::string &sessionID,
                                     const std::string &host, uint16_t port,
                                     bool silent = false);

    static std::string namingLookup(const std::string &name);

    static eStatus checkAnswer(const std::string &answer);
    static std::string getValue(const std::string &answer,
                                const std::string &key);

private:
    template<typename... t_args>
    static std::string
    createSAMRequest(const char *msg) { return {msg}; }

    template<typename... t_args>
    static std::string
    createSAMRequest(const char *format, t_args &&... args)
    {
        const int bufferStatus = std::snprintf(nullptr, 0, format, args...);
        if (bufferStatus < 0)
        {
            std::cerr << "Samty::Message::createSAMRequest() Failed to allocate buffer" << std::endl;
            return {};
        }

        std::vector<char> buffer(bufferStatus + 1);
        const int status =
            std::snprintf(buffer.data(), buffer.size(), format, args...);

        if (status < 0)
        {
            std::cerr << "Samty::Message::createSAMRequest() Failed to format message" << std::endl;
            return {};
        }

        return {buffer.data()};
    }
};

class I2pSocket
{
public:
    I2pSocket(const std::string &SAMHost, uint16_t SAMPort);
    explicit I2pSocket(const sockaddr_in &addr); // explicit because we don't want to create any socket implicity
    explicit I2pSocket(I2pSocket&& another) = default; // for moving
    explicit I2pSocket(const I2pSocket &rhs); // creates a new socket with the same parameters
    ~I2pSocket();

    void bootstrapI2P();

    bool write(const std::vector<uint8_t> &data);
    bool write(const std::string &msg);
    std::string readDestination(); // NOT for silence mode!
    std::string readString();
    std::vector<uint8_t> readBinary();
    ssize_t readBytes(uint8_t *buffer, size_t length, int timeoutSec);

    SOCKET release();
    void close();

    bool isOk() const;

    const std::string &getVersion() const;
    const std::string &getHost() const;
    uint16_t getPort() const;

    const sockaddr_in &getAddress() const;

    const std::string minVer_ = "3.0";
    const std::string maxVer_ = "3.1";

    std::string errorString() const { return errorString_; }

private:
    SOCKET socket_;
    sockaddr_in servAddr_;
    std::string SAMHost_;
    uint16_t SAMPort_;
    std::string version_;

    std::string errorString_;

#ifdef WIN32
    static int instances_;
    static void initWSA();
    static void freeWSA();
#endif // WIN32

    void handshake();
    void init();
};

template<class T>
struct RequestResult
{
    bool isOk;
    T value;

    RequestResult() : isOk(false) {}

    explicit RequestResult(const T &value) : isOk(true), value(value) {}
};

template<class T>
struct RequestResult<std::unique_ptr<T>>
{
    /**
   * a class-helper for resolving a problem with conversion
   * from temporary RequestResult to non-const RequestResult&
   */
    struct RequestResultRef
    {
        bool isOk;
        T *value;

        RequestResultRef(bool isOk, T *value) : isOk(isOk), value(value) {}
    };

    bool isOk;
    std::unique_ptr<T> value;

    RequestResult() : isOk(false) {}

    explicit RequestResult(std::unique_ptr<T> &&value)
        : isOk(true), value(std::move(value)) {}

    RequestResult(RequestResultRef ref) : isOk(ref.isOk), value(ref.value) {}

    RequestResult &operator=(RequestResultRef ref)
    {
        if (value.get() != ref.value)
        {
            isOk = ref.isOk;
            value.reset(ref.value);
        }
        return *this;
    }

    operator RequestResultRef()
    {
        return RequestResultRef(this->isOk, this->value.release());
    }
};

template<>
struct RequestResult<void>
{
    bool isOk;

    RequestResult() : isOk(false) {}

    explicit RequestResult(bool isOk) : isOk(isOk) {}
};


class SAMSession
{
public:
    SAMSession(const Configuration& configuration);
    SAMSession(SAMSession& rhs);
    virtual ~SAMSession() = default;

    static std::string generateSessionID();
    RequestResult<const std::string> namingLookup(const std::string &name) const;

    Identity createSession(const std::string &destination);
    Identity createSession(const std::string &destination, const std::string &sigType);
    virtual Identity createSession(const std::string &destination, const std::string &sigType, const std::string &i2pOptions) = 0;

    const Identity &getMyDestination() const { return myDestination_; }

    const sockaddr_in &getSAMAddress() const { return socket_.getAddress(); }
    const std::string &getSAMHost() const { return socket_.getHost(); }
    uint16_t getSAMPort() const { return socket_.getPort(); }
    const Configuration &getConfiguration() const { return config_; }
    const std::string &getSessionID() const { return sessionID_; }
    const std::string &getSAMMinVer() const { return socket_.minVer_; }
    const std::string &getSAMMaxVer() const { return socket_.maxVer_; }
    const std::string &getSAMVersion() const { return socket_.getVersion(); }
    const std::string &getOptions() const { return i2pOptions_; }

    bool isSick() const { return isSick_; }

protected:
    static Message::Answer<const std::string> rawRequest(I2pSocket &socket, const std::string &requestStr);
    static Message::Answer<const std::string> request(I2pSocket &socket, const std::string &requestStr, const std::string &keyOnSuccess);
    static Message::eStatus request(I2pSocket &socket, const std::string &requestStr);

    static Message::Answer<const std::string> namingLookup(I2pSocket &socket, const std::string &name);

    void fallSick() const;

    I2pSocket socket_;
     const std::string sessionID_;
    Identity myDestination_;
    std::string i2pOptions_;
    mutable bool isSick_;
    const Configuration config_;
};

class StreamSession : public SAMSession
{
public:
    StreamSession(const Configuration& configuration);
    explicit StreamSession(StreamSession &rhs);
    ~StreamSession();

    RequestResult<std::unique_ptr<I2pSocket>> accept(bool silent);
    RequestResult<std::unique_ptr<I2pSocket>> connect(const std::string &destination, bool silent);
    RequestResult<void> forward(const std::string &host, uint16_t port, bool silent);
    void closeSocket();

    std::string errorString() const;

    void stopForwarding(const std::string &host, uint16_t port);
    void stopForwardingAll();

private:
    StreamSession(const StreamSession &rhs);
    StreamSession &operator=(const StreamSession &rhs);

    struct ForwardedStream
    {
        I2pSocket *socket;
        std::string host;
        uint16_t port;
        bool silent;

        ForwardedStream(I2pSocket *socket, const std::string &host, uint16_t port,
                        bool silent)
            : socket(socket), host(host), port(port), silent(silent) {}
    };

    typedef std::list<ForwardedStream> ForwardedStreamsContainer;

    ForwardedStreamsContainer forwardedStreams_;

    Identity createStreamSession(const std::string &destination, const std::string &sigType, const std::string &i2pOptions);
    Identity createSession(const std::string &destination, const std::string &sigType, const std::string &i2pOptions) override;

    // commands
    static Message::Answer<const std::string> createStreamSession(
        I2pSocket &socket, const std::string &sessionID,
        const std::string &nickname, const std::string &destination,
        const std::string &options, const std::string &signatureType);

    static Message::eStatus accept(I2pSocket &socket,
                                   const std::string &sessionID, bool silent);
    static Message::eStatus connect(I2pSocket &socket,
                                    const std::string &sessionID,
                                    const std::string &destination, bool silent);
    static Message::eStatus forward(I2pSocket &socket,
                                    const std::string &sessionID,
                                    const std::string &host, uint16_t port,
                                    bool silent);
};

} // namespace Samty
